/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.imp.dbase;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;

import org.apache.commons.io.EndianUtils;
import org.apache.log4j.Logger;

import who.tmap.common.TMapException;
import who.tmap.dao.FileExtensions;
import who.tmap.imp.dbase.impl.DBaseObjectFactory;


/**
 * A helper to read dBase file
 * File format specifications found at http://www.pgts.com.au/download/public/xbase.htm
 * and http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT
 * @author stl
 *
 */
public class DBaseFileReader implements DBaseFieldTypes {

    private static Logger logger =Logger.getLogger(DBaseFileReader.class);
    private static DBaseObjectFactory dbof =DBaseObjectFactory.getInstance();
    
    private static final byte DBASE_FILE_CODE =3;    
    
    private File file;
    private FileInputStream fis;
    private DataInputStream dis;      
    
    private DBaseHeader header =null;
    private DBaseFieldDescriptor[] fieldsDesc =null;
    private Object[] records =null;
    
    public DBaseFileReader() {
    }
    
    public DBaseFile readDBaseFile( File file ) throws TMapException {
    	try {
    		openFile( file );
    		
    		readHeader();
    		readFieldDescriptors();
    		readRecords();
    	} catch( Exception e ) {
    		logger.error( e );
    	} finally {
    		try {
    			closeFile();
    		} catch( Exception e2 ) {
    			// there is no point logging an error here
    			return null;
    		}
    	}
    	
    	DBaseFile dbaseFile =(DBaseFile)dbof.get( DBaseObjects.DBASE_FILE );
    	dbaseFile.setFile( file );
    	dbaseFile.setHeader( header );
    	dbaseFile.setFieldsDescriptor( fieldsDesc );
    	dbaseFile.setRecords( records );
    	
    	return dbaseFile;
    }
    
    public File convertShapeFileNameToDBaseFileName( File shapeFile ) throws IOException {
    	String dbName =shapeFile.getCanonicalPath()
    							.substring( 0, shapeFile.getCanonicalPath().length() - 4 );
    	dbName+=FileExtensions.DBF_EXT;
    	
    	return new File( dbName );
    }
    
    public DBaseHeader getHeader() {
    	return header;
    }
    
    public DBaseFieldDescriptor[] getFieldsDescriptor() {
    	return fieldsDesc;
    }
    
    public Object[] getRecords() {
    	return records;
    }
    
    /**
     * Open the dbase file
     * @param file
     * @throws FileNotFoundException
     */
    public void openFile( File file ) throws FileNotFoundException {
        this.file =file;
        
        fis = new FileInputStream( file );
        dis = new DataInputStream( fis );
    }
    
    /**
     * Close the dbase file
     * @throws IOException
     */
    public void closeFile() throws IOException {
        dis.close();
        fis.close();
    }    
    
    public DBaseHeader readHeader() throws IOException, TMapException {
    	
    	header =(DBaseHeader)dbof.get( DBaseObjects.DBASE_HEADER );
    	
    	header.setFileCode( dis.readByte() );
    	if ( header.getFileCode() != DBASE_FILE_CODE ) {
    		logger.warn( "Invalid dbase file code [" + header.getFileCode() + "]" );
    	}
    	header.setYearOfLastUpdate( dis.readByte() );
    	header.setMonthOfLastUpdate( dis.readByte() );
    	header.setDayOfLastUpdate( dis.readByte() );
    	
    	header.setNbRecords( readIntLittleEndian() );
    	header.setHeaderLength( readShortLittleEndian() );
    	header.setRecordLength( readShortLittleEndian() );
    	
    	header.setReserved1213( readShortLittleEndian() );
    	header.setIncompleteTransaction( dis.readByte() );
    	header.setEncryptionFlag( dis.readByte() );
    	
    	header.setFreeRecordThread( readIntLittleEndian() );
    	header.setMultiUser( readDoubleLittleEndian() );
    	
    	header.setMdxFlag( dis.readByte() );
    	header.setLanguageDriver( dis.readByte() );
    	header.setReserved3031( readShortLittleEndian() );
    	
    	return header;
    	
    }
    
    public DBaseFieldDescriptor[] readFieldDescriptors() throws IOException, TMapException {
    	List<DBaseFieldDescriptor> list =new ArrayList<DBaseFieldDescriptor>();
    	
    	DBaseFieldDescriptor fieldDesc =null;
    	do {
    		fieldDesc =readFieldDescriptor();
    		if ( fieldDesc != null ) {
    			list.add( fieldDesc );
    		}
    	} while( fieldDesc != null );
    	
    	fieldsDesc =new DBaseFieldDescriptor[ list.size() ];
    	fieldsDesc =(DBaseFieldDescriptor[])list.toArray(fieldsDesc);
    	
    	return fieldsDesc;
    }
    
    private DBaseFieldDescriptor readFieldDescriptor() throws IOException, TMapException {
    	DBaseFieldDescriptor desc =(DBaseFieldDescriptor)dbof.get( DBaseObjects.DBASE_FIELD_DESCRIPTOR );
    	
    	boolean nameCompleted =false;
    	StringBuffer name =new StringBuffer();
    	byte chr =0;
    	// Name is coded on ten fixed position, with byte 0 ending
    	for( int ct =0; ct < 11; ct++ ) {
    		chr =dis.readByte();
    		
    		// name completed
    		if ( chr == 0 ) {
    			nameCompleted =true;
    		}
    		
    		// End of the field descriptor structure
    		if ( "".equals( name.toString() ) && chr == 13  ) {
    			return null;
    		}
    		
    		if ( !nameCompleted ) {
    			name.append( (char)chr );
    		} 
    	}

    	desc.setName( name.toString().trim() );
    	
    	byte type =dis.readByte();
    	desc.setType( String.valueOf( (char)type ) );
    	
    	desc.setDataAddress( readIntLittleEndian() );
    	desc.setLength( dis.readByte() );
    	desc.setDecimalCount( dis.readByte() );
    	desc.setMultiUser1819( readShortLittleEndian() );
    	desc.setWorkAreaId( dis.readByte() );
    	desc.setMultiUser2122( readShortLittleEndian() );
    	desc.setSetFields( dis.readByte() );
    	desc.setReserved24252627( readIntLittleEndian() );
    	desc.setReserved2829( readShortLittleEndian() );
    	desc.setReserved30( dis.readByte() );
    	desc.setIndexFieldFlag( dis.readByte() );
    	
    	return desc;
    }
    
    @SuppressWarnings("unchecked")
	public Object[] readRecords() throws IOException {
    	List list =new ArrayList();
    	
    	for( int ct =0; ct < header.getNbRecords(); ct++ ) {
    		Object[] record =readRecord();
    		// if record not deleted
    		if ( record != null ) {
    			list.add( record );
    		}
    	}
    	
    	byte endOfFile =-1;
    	try {
	    	endOfFile =dis.readByte();
    	} catch( Exception e ) {
    		// nothing to log in here !
    	}
    	
    	if ( endOfFile != 26 ) {
    		logger.warn( "Could not find end of file character!" );
    	}    	
    	
    	records =new Object[ list.size() ];
    	records =(Object[])list.toArray(records);    	
    	
    	return records;
    }
    
    private Object[] readRecord() throws IOException {
    	Object[] record =new Object[ fieldsDesc.length ];
    	
    	byte deleted =dis.readByte();   		
    		
		for( int fieldCt =0; fieldCt < fieldsDesc.length; fieldCt++ ) {
		    
			int fieldLength =fieldsDesc[ fieldCt ].getLength();
			Object obj =null;
			
		    switch ( fieldsDesc[fieldCt].getType().toUpperCase().charAt(0) ) {
		        	        
		      case LOGICAL:
		    	  obj =readBoolean();
		    	  break;
		        
		      case NUMBER: 
			      if (fieldsDesc[fieldCt].getDecimalCount() == 0) {
		        	String str =readString( fieldLength ).trim();
		        	try {
		        		obj =Integer.valueOf( str );
		        	} catch( Exception e ) {
		        		logger.warn( "Could not convert dbase field to Integer [" + str + "]" );
		        		obj =new Integer(0);
		        	}
			      } else {
		        	String str =readString( fieldLength ).trim();
		        	try {
		        		obj =Double.valueOf( str );
		        	} catch( Exception e ) {
		        		logger.warn( "Could not convert dbase field convert to Double [" + str + "]" );
		        		obj =new Double(0);
		        	}
			      }
			      break;
			        
		      case FLOATING_POINT: 
		    	  	String str =readString( fieldLength ).trim();
		        	try {
		        		obj =Double.valueOf( str );
		        	} catch( Exception e ) {
		        		logger.warn( "Could not convert dbase field to Double [" + str + "]" );		        		
		        		obj =new Double(0);
		        	}
			        break;		        
		        
		      case DATE:
			      String date =readString( fieldLength );		    	  
		    	  try {
			        int year =Integer.parseInt( date.substring( 0, 4 ) );
			        int month =Integer.parseInt( date.substring( 4, 2 ) );
			        int day =Integer.parseInt( date.substring( 6, 2 ) );
			        Calendar cal =new GregorianCalendar( year, month-1, day );
			        obj =cal.getTime();
		    	  } catch( Exception e ) {
		        	logger.warn( "Could not convert dbase field to Date [" + date + "]" );  
		    		obj =Calendar.getInstance().getTime();  
		    	  }
			      break;
		        
		      case CHARACTER: 
		      default:
			    	obj =readString( fieldLength ).trim();
			        break;
		    }    	
		    
		    record[ fieldCt ] =obj;
			
		}
		
		if ( deleted != 32 ) {
			record =null;
		}
    	
    	return record;
    }
    
    private Boolean readBoolean() throws IOException {
    	char boolC =(char)dis.readByte();
    	String boolS =String.valueOf( boolC ).toUpperCase();
    	boolean bool ="T".equals( boolS ) || "Y".equals( boolS ); 
    	
    	return Boolean.valueOf( bool );
    }
    
    private String readString( int length ) throws IOException {
    	StringBuffer str =new StringBuffer();
    	for( int ct =0; ct < length; ct++ ) {
    		str.append( (char)dis.readUnsignedByte() );
    	}
    	
    	return str.toString();
    }
    
    /**
     * Reads a little endian short
     * @return little endian short
     * @throws IOException
     */
    private short readShortLittleEndian() throws IOException
    {
    	return EndianUtils.readSwappedShort( dis );    	
    }    
    
    /**
     * Reads a little endian int
     * @return little endian int
     * @throws IOException
     */
    private int readIntLittleEndian() throws IOException
    {
	    return EndianUtils.readSwappedInteger( dis );
    }
    
    /**
     * Reads a little endian double
     * @return little endian double
     * @throws IOException
     */
    double readDoubleLittleEndian() throws IOException
    {
    	return EndianUtils.readSwappedDouble( dis );
    }       

}
